Skip to content

Filter MCPServer capabilities by registered primitives#2477

Open
MukundaKatta wants to merge 1 commit intomodelcontextprotocol:mainfrom
MukundaKatta:codex/mcpserver-capability-filter
Open

Filter MCPServer capabilities by registered primitives#2477
MukundaKatta wants to merge 1 commit intomodelcontextprotocol:mainfrom
MukundaKatta:codex/mcpserver-capability-filter

Conversation

@MukundaKatta
Copy link
Copy Markdown

Summary

  • stop MCPServer from advertising prompt, resource, and tool capabilities when nothing has been registered for that primitive
  • keep the low-level Server behavior unchanged by making the filtering opt-in
  • add coverage for empty, tool-only, prompt-only, and resource-template-only MCPServer initialization

Testing

  • python3 -m py_compile src/mcp/server/lowlevel/server.py src/mcp/server/mcpserver/server.py tests/server/mcpserver/test_server.py
  • pytest tests/server/mcpserver/test_server.py -k capability (not run locally: pytest is not installed in this environment)

@rendina-io
Copy link
Copy Markdown

Advertising capabilities that have no registered handlers is a sneaky source of compatibility issues — clients that enumerate capabilities and then try to call an unregistered primitive get confusing 'method not found' errors instead of a clean capability negotiation failure. Filtering at the capability advertisement level is the right place to fix this. Does the change affect the list_changed notification behavior, or is that still gated on the same registered-primitive check?

@MukundaKatta
Copy link
Copy Markdown
Author

@rendina-io Good question — short answer: list_changed advertisement goes along with the capability, but the notification sending path is untouched.

The filter I added (MCPServer._filter_capabilities) only runs after the low-level server has already built ServerCapabilities in Server.get_capabilities (server.py:285-321). That existing code is what computes tools_capability = ToolsCapability(list_changed=notification_options.tools_changed) (and the prompt/resource equivalents). My filter then conditionally zeroes the whole capability (capabilities.tools = None) when zero tools are registered on the high-level MCPServer. So:

  • Zero tools registeredcapabilities.tools is None entirely, which means the list_changed field naturally disappears with it. That's the behavior we want: if there are no tools, there's no meaningful "tools list changed" to notify about, so not advertising it is consistent.
  • ≥1 tool registered → capability is left exactly as the low-level server produced it, including its list_changed flag based on notification_options.tools_changed.

The actual notifications/tools/list_changed sending path (wherever the server emits that when the registered list mutates) isn't touched by this PR at all — it's still gated on whatever conditions it had before. So filtering at advertisement time gives clients a clean negotiation without silently dropping notifications that used to fire.

Happy to add a test that nails that invariant down (e.g. assert that with zero registered tools, initialize responds with capabilities.tools is None and therefore tools.listChanged is absent) if that'd be useful — let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants